package run.halo.app.service.impl;

import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import run.halo.app.exception.NotFoundException;
import run.halo.app.model.dto.CategoryWithSiteCountDTO;
import run.halo.app.model.entity.Category;
import run.halo.app.model.entity.Site;
import run.halo.app.model.entity.SiteCategory;
import run.halo.app.model.enums.SiteStatus;
import run.halo.app.model.projection.CategoryIdSiteStatusProjection;
import run.halo.app.model.vo.CategoryVO;
import run.halo.app.repository.SiteCategoryRepository;
import run.halo.app.repository.SiteRepository;
import run.halo.app.service.CategoryService;
import run.halo.app.service.OptionService;
import run.halo.app.service.SiteCategoryService;
import run.halo.app.service.base.AbstractCrudService;
import run.halo.app.utils.HaloUtils;
import run.halo.app.utils.ServiceUtils;

/**
 * Site category service implementation.
 *
 * @author meazty
 * @since 2022/7/30 14:59
 **/
@Service
public class SiteCategoryServiceImpl extends AbstractCrudService<SiteCategory, Integer>
    implements SiteCategoryService {

    private final SiteCategoryRepository siteCategoryRepository;

    private final SiteRepository siteRepository;

    private CategoryService categoryService;

    private final OptionService optionService;

    public SiteCategoryServiceImpl(SiteCategoryRepository siteCategoryRepository,
        SiteRepository siteRepository,
        OptionService optionService) {
        super(siteCategoryRepository);
        this.siteCategoryRepository = siteCategoryRepository;
        this.siteRepository = siteRepository;
        this.optionService = optionService;
    }

    @Lazy
    @Autowired
    public void setCategoryService(CategoryService categoryService) {
        this.categoryService = categoryService;
    }

    @Override
    public List<Category> listCategoriesBy(Integer siteId) {
        Assert.notNull(siteId, "Site id must not be null");

        // Find all category ids
        Set<Integer> categoryIds = siteCategoryRepository.findAllCategoryIdsBySiteId(siteId);

        return categoryService.listAllByIds(categoryIds);
    }


    @Override
    public Map<Integer, List<Category>> listCategoryListMap(
        Collection<Integer> siteIds) {
        if (CollectionUtils.isEmpty(siteIds)) {
            return Collections.emptyMap();
        }

        // Find all site categories
        List<SiteCategory> siteCategories = siteCategoryRepository.findAllBySiteIdIn(siteIds);

        // Fetch category ids
        Set<Integer> categoryIds =
            ServiceUtils.fetchProperty(siteCategories, SiteCategory::getCategoryId);

        // Find all categories
        List<Category> categories = categoryService.listAllByIds(categoryIds);

        // Convert to category map
        Map<Integer, Category> categoryMap = ServiceUtils.convertToMap(categories, Category::getId);

        // Create category list map
        Map<Integer, List<Category>> categoryListMap = new HashMap<>(16);

        // Foreach and collect
        siteCategories.forEach(siteCategory -> categoryListMap
            .computeIfAbsent(siteCategory.getSiteId(), siteId -> new LinkedList<>())
            .add(categoryMap.get(siteCategory.getCategoryId())));

        return categoryListMap;
    }

    @Override
    public List<Site> listSiteBy(Integer categoryId) {
        Assert.notNull(categoryId, "Category id must not be null");

        // Find all site ids
        Set<Integer> siteIds = siteCategoryRepository.findAllSiteIdsByCategoryId(categoryId);

        return siteRepository.findAllById(siteIds);
    }

    @Override
    public List<Site> listSiteBy(Integer categoryId, SiteStatus status) {
        Assert.notNull(categoryId, "Category id must not be null");
        Assert.notNull(status, "Site status must not be null");

        // Find all site ids
        Set<Integer> siteIds =
            siteCategoryRepository.findAllSiteIdsByCategoryId(categoryId, status);

        return siteRepository.findAllById(siteIds);
    }

    @Override
    public List<Site> listSiteBy(Integer categoryId, Set<SiteStatus> status) {
        Assert.notNull(categoryId, "Category id must not be null");
        Assert.notNull(status, "Site status must not be null");

        // Find all site ids
        Set<Integer> siteIds = siteCategoryRepository
            .findAllSiteIdsByCategoryId(categoryId, status);

        return siteRepository.findAllById(siteIds);
    }

    @Override
    public List<Site> listSiteBy(String slug, Set<SiteStatus> status) {
        Assert.notNull(slug, "Category slug must not be null");
        Assert.notNull(status, "Site status must not be null");

        Category category = categoryService.getBySlug(slug);

        if (Objects.isNull(category)) {
            throw new NotFoundException("查询不到该分类的信息").setErrorData(slug);
        }

        Set<Integer> sitesIds = siteCategoryRepository
            .findAllSiteIdsByCategoryId(category.getId(), status);

        return siteRepository.findAllById(sitesIds);
    }

    @Override
    public List<Site> listSiteBy(String slug, SiteStatus status) {
        Assert.notNull(slug, "Category slug must not be null");
        Assert.notNull(status, "Site status must not be null");

        Category category = categoryService.getBySlug(slug);

        if (Objects.isNull(category)) {
            throw new NotFoundException("查询不到该分类的信息").setErrorData(slug);
        }

        Set<Integer> sitesIds =
            siteCategoryRepository.findAllSiteIdsByCategoryId(category.getId(), status);

        return siteRepository.findAllById(sitesIds);
    }

    @Override
    public Page<Site> pageSiteBy(Integer categoryId, Pageable pageable) {
        Assert.notNull(categoryId, "Category id must not be null");
        Assert.notNull(pageable, "Page info must not be null");

        // Find all site ids
        Set<Integer> siteIds = siteCategoryRepository.findAllSiteIdsByCategoryId(categoryId);

        return siteRepository.findAllByIdIn(siteIds, pageable);
    }

    @Override
    public Page<Site> pageSiteBy(Integer categoryId, SiteStatus status, Pageable pageable) {
        Assert.notNull(categoryId, "Category id must not be null");
        Assert.notNull(status, "Site status must not be null");
        Assert.notNull(pageable, "Page info must not be null");

        // Find all site ids
        Set<Integer> siteIds = siteCategoryRepository
            .findAllSiteIdsByCategoryId(categoryId, status);

        return siteRepository.findAllByIdIn(siteIds, pageable);
    }

    @Override
    public Page<Site> pageSiteBy(Integer categoryId, Set<SiteStatus> status, Pageable pageable) {
        Assert.notNull(categoryId, "Category id must not be null");
        Assert.notNull(status, "Site status must not be null");
        Assert.notNull(pageable, "Page info must not be null");

        // Find all site ids
        Set<Integer> siteIds =
            siteCategoryRepository.findAllSiteIdsByCategoryId(categoryId, status);

        return siteRepository.findAllByIdIn(siteIds, pageable);
    }

    @Override
    public List<SiteCategory> mergeOrCreateByIfAbsent(Integer siteId, Set<Integer> categoryIds) {
        Assert.notNull(siteId, "Site id must not be null");

        if (CollectionUtils.isEmpty(categoryIds)) {
            return Collections.emptyList();
        }

        // Build site categories
        List<SiteCategory> siteCategoriesStaging = categoryIds.stream().map(categoryId -> {
            SiteCategory siteCategory = new SiteCategory();
            siteCategory.setSiteId(siteId);
            siteCategory.setCategoryId(categoryId);
            return siteCategory;
        }).collect(Collectors.toList());

        List<SiteCategory> siteCategoriesToCreate = new LinkedList<>();
        List<SiteCategory> siteCategoriesToRemove = new LinkedList<>();

        // Find all exist site categories
        List<SiteCategory> siteCategories = siteCategoryRepository.findAllBySiteId(siteId);

        siteCategories.forEach(siteCategory -> {
            if (!siteCategoriesStaging.contains(siteCategory)) {
                siteCategoriesToRemove.add(siteCategory);
            }
        });

        siteCategoriesStaging.forEach(siteCategoryStaging -> {
            if (!siteCategories.contains(siteCategoryStaging)) {
                siteCategoriesToCreate.add(siteCategoryStaging);
            }
        });

        // Remove site categories
        removeAll(siteCategoriesToRemove);

        // Remove all site categories need to remove
        siteCategories.removeAll(siteCategoriesToRemove);

        // Add all created site categories
        siteCategories.addAll(createInBatch(siteCategoriesToCreate));

        // Create them
        return siteCategories;
    }

    @Override
    public List<SiteCategory> listBySiteId(Integer siteId) {
        Assert.notNull(siteId, "Site id must not be null");

        return siteCategoryRepository.findAllBySiteId(siteId);
    }

    @Override
    public List<SiteCategory> listByCategoryId(Integer categoryId) {
        Assert.notNull(categoryId, "Category id must not be null");

        return siteCategoryRepository.findAllByCategoryId(categoryId);
    }

    @Override
    public Set<Integer> listCategoryIdsBySiteId(Integer siteId) {
        Assert.notNull(siteId, "Site id must not be null");

        return siteCategoryRepository.findAllCategoryIdsBySiteId(siteId);
    }

    @Override
    public List<SiteCategory> removeBySiteId(Integer siteId) {
        Assert.notNull(siteId, "Site id must not be null");

        return siteCategoryRepository.deleteBySiteId(siteId);
    }

    @Override
    public List<SiteCategory> removeByCategoryId(Integer categoryId) {
        Assert.notNull(categoryId, "Category id must not be null");

        return siteCategoryRepository.deleteByCategoryId(categoryId);
    }

    @Override
    public List<CategoryWithSiteCountDTO> listCategoryWithSiteCountDto(@NonNull Sort sort) {
        Assert.notNull(sort, "Sort info must not be null");
        List<Category> categories = categoryService.listAll(sort);
        List<CategoryVO> categoryTreeVo = categoryService.listToTree(categories);
        populateSiteIds(categoryTreeVo, siteStatus -> !SiteStatus.RECYCLE.equals(siteStatus));
        // Convert and return
        return flatTreeToList(categoryTreeVo);
    }

    private List<CategoryWithSiteCountDTO> flatTreeToList(List<CategoryVO> categoryTree) {
        Assert.notNull(categoryTree, "The categoryTree must not be null.");
        List<CategoryWithSiteCountDTO> result = new LinkedList<>();
        walkCategoryTree(categoryTree, category -> {
            CategoryWithSiteCountDTO categoryWithSiteCountDto =
                new CategoryWithSiteCountDTO();
            BeanUtils.copyProperties(category, categoryWithSiteCountDto);
            String fullPath = categoryService.buildCategoryFullPath(category.getSlug());
            categoryWithSiteCountDto.setFullPath(fullPath);
            // populate site count.
            int siteCount = Objects.requireNonNullElse(category.getSiteIds(),
                Collections.emptySet()).size();
            categoryWithSiteCountDto.setSiteCount((long) siteCount);
            result.add(categoryWithSiteCountDto);
        });
        return result;
    }

    private void populateSiteIds(List<CategoryVO> categoryTree,
        Predicate<SiteStatus> statusFilter) {
        Assert.notNull(categoryTree, "The categoryTree must not be null.");
        Map<Integer, Set<Integer>> categorySiteIdsMap =
            siteCategoryRepository.findAllWithSiteStatus()
                .stream()
                .filter(record -> statusFilter.test(record.getSiteStatus()))
                .collect(Collectors.groupingBy(CategoryIdSiteStatusProjection::getCategoryId,
                    Collectors.mapping(CategoryIdSiteStatusProjection::getSiteId,
                        Collectors.toSet())));

        walkCategoryTree(categoryTree, category -> {
            // Set site count
            Set<Integer> siteIds =
                categorySiteIdsMap.getOrDefault(category.getId(), new LinkedHashSet<>());
            category.setSiteIds(siteIds);
        });
        CategoryVO categoryTreeRootNode = new CategoryVO();
        categoryTreeRootNode.setChildren(categoryTree);
        mergeSiteIdsFromBottomToTop(categoryTreeRootNode);
    }

    private void mergeSiteIdsFromBottomToTop(CategoryVO root) {
        if (root == null) {
            return;
        }
        List<CategoryVO> children = root.getChildren();
        if (CollectionUtils.isEmpty(children)) {
            return;
        }
        for (CategoryVO category : children) {
            mergeSiteIdsFromBottomToTop(category);
            if (root.getSiteIds() == null) {
                root.setSiteIds(new LinkedHashSet<>());
            }
            // merge site ids.
            root.getSiteIds().addAll(category.getSiteIds());
        }
    }

    private void walkCategoryTree(List<CategoryVO> categoryTree, Consumer<CategoryVO> consumer) {
        Queue<CategoryVO> queue = new ArrayDeque<>(categoryTree);
        while (!queue.isEmpty()) {
            CategoryVO category = queue.poll();
            consumer.accept(category);
            if (HaloUtils.isNotEmpty(category.getChildren())) {
                queue.addAll(category.getChildren());
            }
        }
    }

    @Override
    public List<SiteCategory> listByCategoryIdList(List<Integer> categoryIdList) {
        Assert.notEmpty(categoryIdList, "category id list not empty");
        return siteCategoryRepository.findAllByCategoryIdList(categoryIdList);
    }

}
